feat: opt-in pipeline fit optimisations, native bucketize, and sampled fitting support with caching - #69
feat: opt-in pipeline fit optimisations, native bucketize, and sampled fitting support with caching#69ConorWorthington wants to merge 31 commits into
Conversation
Plans are too slow to materialise - this is our attempt to speed it up
Wrap the moments aggregation in StandardScale, SingleFeatureArrayStandardScale and ConditionalStandardScale estimators in a guarded persist/unpersist so the array-size probe and the aggregation reuse a materialised result instead of re-scanning the upstream lineage twice. Repair the incomplete persist edit in ConditionalStandardScale._fit. Add checkpointInterval / pruneInputColumns coverage to the pipeline tests and a checkpoint directory to the spark_session fixture. Surface estimator fit errors as RuntimeError chained from the original exception. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Merge latest version
georyetti
left a comment
There was a problem hiding this comment.
Mostly minor changes to pipeline logic. Also bucketize changes I assume should not be here. Lastly you need to run a uv lock.
| """ | ||
| super().__init__(stages=stages) | ||
| kwargs = self._input_kwargs | ||
| super().__init__() |
There was a problem hiding this comment.
The new super call drops the stages=stages, was this intended?
| kwargs = self._input_kwargs | ||
| super().__init__() | ||
| self._setDefault( | ||
| checkpointInterval=0, |
There was a problem hiding this comment.
Default to None instead of 0?
| for param in stage.params: | ||
| if not (param.name.endswith("Col") or param.name.endswith("Cols")): | ||
| continue | ||
| if not stage.isDefined(param): | ||
| continue | ||
| value = stage.getOrDefault(param) | ||
| if isinstance(value, str): | ||
| required_input_columns.add(value) | ||
| elif isinstance(value, (list, tuple)): | ||
| required_input_columns.update( | ||
| item for item in value if isinstance(item, str) | ||
| ) |
There was a problem hiding this comment.
This feels more complex than I think it needs to be. Two things:
- Why do we need to iterate through the stage.params at all if we have already added the inputs using
get_layer_inputs_outputs - Even if we do need, we can just check for presence of
inputColorinputColsas a stage always has strictly one of these defined.if stage.hasParam("inputCol") and stage.isDefined("inputCol"): value = stage.getInputCol()and after check input cols. But this is really whatget_layer_inputs_outputsdoes.
There was a problem hiding this comment.
You're right that inputCol/inputCols are already covered by get_layer_inputs_outputs but the sweep isn't for those.
It's there for the other column params a stage reads that aren't its input col: maskCols/relevanceCol on ConditionalStandardScaleEstimator, and queryIdCol on the listwise transformers. Those get read at fit time, so if we don't keep them, pruneInputColumns=True drops them and the fit blows up with "column not found". There are regression tests covering exactly that.
If the suffix-matching feels too hacky, I'm happy to swap it for a small get_fit_input_columns() hook on the handful of stages that need it instead. Let me know what you'd prefer.
| """ | ||
| required_input_columns = self.collect_required_input_columns(stages) | ||
| columns_to_keep = [c for c in dataset.columns if c in required_input_columns] | ||
| if columns_to_keep and len(columns_to_keep) < len(dataset.columns): |
There was a problem hiding this comment.
Pedantic but do we need that second condition? If we are inside this function then we are pruning, and columns_to_keep is always a subset. So I would just check it's not empty and otherwise select
| fit. 0 (or None) disables checkpointing. | ||
| :returns: KamaeSparkPipeline object with checkpointInterval set. | ||
| """ | ||
| return self._set(checkpointInterval=value) |
There was a problem hiding this comment.
We should error on checkpoint interval being negative here. Personally I think we should error on 0 too and treat None as the no checkpoint behaviour
| :returns: KamaeSparkPipeline object with params set. | ||
| """ | ||
| kwargs = self._input_kwargs | ||
| return self._set(**kwargs) |
There was a problem hiding this comment.
This set params does not use the setter methods at all. So any validation in them will not be respected when the user passes arguments to the init as opposed to using the setter method. If you check how I defined the setParams for the estimator and transformer you can see I use the setter method.
| return None | ||
| # We add 1 because we want to reserve the 0 index for mask/padding. | ||
| return bisect_right(splits, value) + 1 | ||
| def bucketize(value: Column) -> Column: |
There was a problem hiding this comment.
Same to assume that all this bucket logic changes to this transformer are not meant to be in this PR?
There was a problem hiding this comment.
I was looking for inefficiencies in the library... I was going to patch more but only bucketize was really impacted in a way I could speed up simply. We don't really use it but figured it was a good improvement so may as well include.
| dependencies = [ | ||
| "pyspark>=3.4.0,<4.0.0", | ||
| "pandas>=1.3.4,<3.0.0", | ||
| "pyarrow>=4.0.0", |
There was a problem hiding this comment.
Adding new dependencies needs a uv lock pls
There was a problem hiding this comment.
Lock updated
- Restore stages=stages in __init__ super call - checkpointInterval defaults to None; reject non-positive via setter - Route setParams through setter methods so validation runs - Drop redundant length check in prune_unused_input_columns - Regenerate uv.lock to include pyarrow (required by pandas_udf) Retains the aux-column sweep in collect_required_input_columns: it is load-bearing for pruning correctness (maskCols/relevanceCol/queryIdCol are not returned by get_layer_inputs_outputs) and defended by regression tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Description
Provide a short description of the PR changes.
The below checklists come from the docs page on adding new transformers here
Keras Layer Checklist
Verify that:
_callmethod has been implemented in the new layer.compatible_dtypesproperty is defined in the new layer.@tf.keras.utils.register_keras_serializable(package=kamae.__name__).name,input_dtype, andoutput_dtypeas arguments to the constructor and that this is passed to the super constructor.get_configmethod.layersdirectory.Spark Transformer/Estimator Checklist
Verify that:
__init__andsetParamsmethods.Paramsclass here.compatible_dtypesproperty has been implemented to specify the input/output data types that my transformer/estimator supports.get_tf_layermethod.transformers/estimatorsdirectory.Finally, please verify that: